#include "Level.h"
#include "Tile.h"
#include "Util.h"
#include "Game.h"
#include "Wall.h"
#include "Npc.h"
#include "Portal.h"
#include "Dungeon.h"
#include "GameState.h"
#include "RandomObjectGenerator.h"
#include "Player.h"
#include "Door.h"
#include "Item.h"

Level::Level(int x, int y, int levelNum, int dlevel, Dungeon* dungeon) {
	m_cols = x;
	m_rows = y;
	m_levelNum = levelNum;
	m_dlevel = dlevel;
	m_npcs = new std::list<Npc*>();
	m_dungeon = dungeon;
}

Level::Level(int x, int y, Level* level) {
	m_cols = x;
	m_rows = y;
	m_levelNum = level->getLevelNum()+1;
	m_dlevel = level->getDLevel()+1;
	m_npcs = new std::list<Npc*>();
	m_dungeon = level->getDungeon();
}

Level::Level(Dungeon* dungeon, std::ifstream& file) {
	std::string line;
	std::getline(file, line);
	int* tokens = Util::intSplit(line);
	m_rows = tokens[0];
	m_cols = tokens[1];
	m_levelNum = tokens[2];
	m_dlevel = tokens[3];
	m_dungeon = dungeon;
	m_npcs = new std::list<Npc*>();

	m_map = new Tile**[m_cols];
	for(int i = 0; i < m_cols; i++) {
		m_map[i] = new Tile*[m_rows];
		for(int j = 0; j < m_rows; j++) {
			m_map[i][j] = new Tile(float(i), float(j), this);
			m_map[i][j]->load(file);
		}
	}
	delete[] tokens;
}

Level::~Level() {
	for(int i = 0; i < m_cols; i++) {
		for(int j = 0; j < m_rows; j++) {
			delete m_map[i][j];
		}
		delete[] m_map[i];
	}
	delete[] m_map;
	delete m_npcs;
}

void Level::save(std::ofstream& file) const {
	file << m_rows << " " << m_cols << " " << m_levelNum << " " << m_dlevel << "\n";
	for(int i = 0; i < m_cols; i++) {
		for(int j = 0; j < m_rows; j++) {
			m_map[i][j]->save(file);
		}
	}
}

void Level::loadPortal(Portal* portal) {
	std::list<Portal*>* portals = (portal->isUp() ? &m_mainUpPortals : &m_mainDownPortals);
	std::list<Portal*>::iterator iter = portals->begin();
	for(; iter != portals->end(); iter++) {
		if(portal->getPortalNum() < (*iter)->getPortalNum())
			break;
	}
	if(iter == portals->begin())
		portals->push_front(portal);
	else if(iter == portals->end())
		portals->push_back(portal);
	else
		portals->insert(iter, portal);
}

void Level::generateLevel() {
	generateLayout();
	generateObjects();
	generateCharacters();
	if(m_levelNum != m_dungeon->getNumLevels()-1) {
		for(int i = 0; i < 3; i++) {
			placePortal(false, i);
		}
	}
}

void Level::generateLayout() {
	m_map = new Tile**[m_cols];
	for(int i = 0; i < m_cols; i++) {
		m_map[i] = new Tile*[m_rows];
		for(int j = 0; j < m_rows; j++) {
			m_map[i][j] = new Tile(TT_NORMAL, float(i), float(j), this);
		}
	}
	for(int i = 0; i < m_rows; i++) {
		m_map[0][i]->addObstacle(new Wall());
		m_map[m_cols-1][i]->addObstacle(new Wall());
	}
	for(int i = 1; i < m_cols-1; i++) {
		m_map[i][0]->addObstacle(new Wall());
		m_map[i][m_rows-1]->addObstacle(new Wall());
	}
}

void Level::generateObjects() {
	int numObstacles = std::max(10, x_between(m_rows*m_cols/100-20, m_rows*m_cols/100));
	for(int i = 0; i < numObstacles; i++) {
		int x = rand(m_cols-2)+1;
		int y = rand(m_rows-2)+1;
		if(m_map[x][y]->isEmpty()) {
			m_map[x][y]->addObstacle(RandomObjectGenerator::getRandomObstacle(m_dlevel));
		}
	}

	int numItems = x_between(m_rows*m_cols/100-10, m_rows*m_cols/100+10);
	for(int i = 0; i < numItems; i++) {
		int x = rand(m_cols-2)+1;
		int y = rand(m_rows-2)+1;
		if(!m_map[x][y]->isOccupied()) {
			Item* item = RandomObjectGenerator::getRandomItem(m_dlevel);
			m_map[x][y]->addItem(item);
		}
	}
}

void Level::placePortal(bool up, int portalNum) {
	int x;
	int y;
	while(true) {
		x = rand(m_cols);
		y = rand(m_rows);
		if(m_map[x][y]->canWalk() && m_map[x][y]->getObstacle() == NULL) {
			Portal* portal = new Portal(up, portalNum, this);
			if(up)
				m_mainUpPortals.push_back(portal);
			else
				m_mainDownPortals.push_back(portal);
			m_map[x][y]->addObstacle(portal);
			break;
		}
	}
}

void Level::placePlayer(Player* player, bool up, int portalNum) {
	g_currentLevel = this;
	std::list<Portal*> m_portalList = (up ? m_mainUpPortals : m_mainDownPortals);
	std::list<Portal*>::iterator iter = m_portalList.begin();
	std::advance(iter, portalNum);
	(*iter)->getTile()->addCharacter(player);
	g_gameState->updateCharacterList();
	g_player->calculateFOV();
}

void Level::generateCharacters() {
	int numCharacters = x_between(m_rows*m_cols/100-20, m_rows*m_cols/100);
	for(int i = 0; i < numCharacters; i++) {
		int x = rand(m_cols-2)+1;
		int y = rand(m_rows-2)+1;
		if(m_map[x][y]->isEmpty() && (m_map[x][y]->getObstacle() == NULL || m_map[x][y]->getObstacle()->getType(TH_GENERIC_TYPE) != GT_OB_PORTAL)) {
			addNpc(RandomObjectGenerator::getRandomNpc(m_dlevel), x,y);
		}
	}
}

void Level::addNpc(Npc* n, int x, int y) {
	m_npcs->push_back(n);
	m_map[x][y]->addCharacter(n);
}

void Level::removeNpc(Npc* npc) {
	m_npcs->remove(npc);
}

std::list<Tile*>* Level::findPath(int startx, int starty, int endx, int endy, Character* c) const {
	std::list<AStarTile*> open, closed;
	insertSort(open, generateAStarTile(m_map[startx][starty], NULL, endx, endy, c));
	AStarTile* currentTile = open.front();
	while((currentTile->h > 2 || (currentTile->h == 2 && (currentTile->tile->getX() == endx || currentTile->tile->getY() == endy))) && currentTile->f < 100) {
		open.remove(currentTile);
		closed.push_back(currentTile);
		int currX = int(currentTile->tile->getX());
		int currY = int(currentTile->tile->getY());
		for(int i = -1; i < 2; i++) {
			for(int j = -1; j < 2; i == 0 ? j+=2 : j++) {
				Tile* currTile = m_map[currX+i][currY+j];
				if(currTile->canWalk() && aStarListContains(closed, currTile) == NULL) {
					if(currTile->getObstacle() == NULL || !currTile->getObstacle()->isBlocking() || (currTile->getObstacle()->getType(TH_GENERIC_TYPE) == GT_OB_DOOR 
							&& !((Door*)currTile->getObstacle())->isOpen() && (((Door*)currTile->getObstacle())->getLockType() == LT_NONE || c->hasKey(((Door*)currTile->getObstacle())->getLockType())))) {
						AStarTile* tile = aStarListContains(open, currTile);
						if(tile == NULL) {
							insertSort(open, generateAStarTile(currTile, currentTile, endx, endy, c));
						} else {
							int newG = generateG(tile, endx, endy, c);
							if(tile->g > newG) {
								tile->g = newG;
								tile->f = tile->g + tile->h;
								open.remove(tile);
								insertSort(open, tile);
								tile->parent = currentTile;
							}
						}
					}
				}
			}
		}
		if(open.size() == 0) {
			currentTile = NULL;
			break;
		}
		currentTile = open.front();
	}

	std::list<Tile*>* path = new std::list<Tile*>();
	while(currentTile != NULL && currentTile->tile != m_map[startx][starty]) {
		if(currentTile->f < 100) {
			path->push_front(currentTile->tile);
		}
		currentTile = currentTile->parent;
	}
	for(std::list<AStarTile*>::iterator iter = open.begin(); iter != open.end(); iter++)
		delete (*iter);
	for(std::list<AStarTile*>::iterator iter = closed.begin(); iter != closed.end(); iter++)
		delete (*iter);
	open.clear();
	closed.clear();
	return path;
}

AStarTile* Level::generateAStarTile(Tile* tile, AStarTile* parent, int endx, int endy, Character* c) {
	AStarTile* aStarTile = new AStarTile;
	aStarTile->tile = tile;
	aStarTile->parent = parent;
	aStarTile->g = generateG(aStarTile, endx, endy, c);
	aStarTile->h = int(std::abs(tile->getX()-endx)+std::abs(tile->getY()-endy));
	aStarTile->f = aStarTile->g + aStarTile->h;
	return aStarTile;
}

void Level::insertSort(std::list<AStarTile*> &tiles, AStarTile* tile) {
	std::list<AStarTile*>::iterator iter = tiles.begin();
	for(; iter != tiles.end(); iter++) {
		if(tile->f > (*iter)->f) {
			continue;
		} else {
			tiles.insert(iter, tile);
			return;
		}
	}
	if(iter == tiles.begin())
		tiles.push_front(tile);
	else
		tiles.push_back(tile);
}

AStarTile* Level::aStarListContains(std::list<AStarTile*> &tiles, Tile* tile) {
	for(std::list<AStarTile*>::iterator iter = tiles.begin(); iter != tiles.end(); iter++) {
		if((*iter)->tile == tile) {
			return *iter;
		}
	}
	return NULL;
}

int Level::generateG(AStarTile* tile, int endx, int endy, Character* c) {
	if(distance(int(tile->tile->getX()), int(tile->tile->getY()), endx, endy) > float(c->getStat(PER))/3.0f+std::log(float(c->getStat(PER))*3))
		return 100;
	if(tile->tile->getCharacter() != NULL)
		return 10;
	return 1;
}

bool Level::lineOfSight(int charX, int charY, int endX, int endY, int viewRadius) const {
	for(int a = 0; a < 2; a++) {
		for(int b = 0; b < 2; b++) {
			int startX = charX+a, startY = charY+b;
			int signX = 0, signY = 0;
			if(endX-startX > 0)
				signX = 1;
			else if(endX-startX < 0)
				signX = -1;
			if(endY-startY > 0)
				signY = 1;
			else if(endY-startY < 0)
				signY = -1;

			if((signX == 1 && signY == 1 && m_map[startX][startY]->blocksLOS())
					|| (signX == -1 && signY == 1 && m_map[startX-1][startY]->blocksLOS())
					|| (signX == -1 && signY == -1 && m_map[startX-1][startY-1]->blocksLOS())
					|| (signX == 1 && signY == -1 && m_map[startX][startY-1]->blocksLOS())) {
				continue;
			}

			int dx = abs(endX - startX);
			int dy = abs(endY - startY);
					
			bool isBlocked = false;
			if(dx != 0) {
				for(int x = 1; x < dx && !isBlocked; x++) {
					int y = x*dy/dx;
					int corner = x*dy%dx;
					int tileX = startX+x*signX, tileY = startY+y*signY;
					if(corner == 0) {
						if(tileX+1 >= m_cols || tileY+1 >= m_rows || tileX <= 0 || tileY <= 0
								|| (m_map[tileX][tileY]->blocksLOS() && m_map[tileX][tileY-1]->blocksLOS())
								|| (m_map[tileX][tileY]->blocksLOS() && m_map[tileX-1][tileY]->blocksLOS())
								|| (m_map[tileX-1][tileY-1]->blocksLOS() && m_map[tileX][tileY-1]->blocksLOS())
								|| (m_map[tileX-1][tileY-1]->blocksLOS() && m_map[tileX-1][tileY]->blocksLOS())) {
							isBlocked = true;
						} else {
							if(signX*signY == 1) {
								if(m_map[tileX][tileY]->blocksLOS() || m_map[tileX-1][tileY-1]->blocksLOS())
									isBlocked = true;
							} else if(signX*signY == -1) {
								if(m_map[tileX][tileY-1]->blocksLOS() || m_map[tileX-1][tileY]->blocksLOS())
									isBlocked = true;
							} else {
								if(m_map[tileX-1][tileY-1]->blocksLOS() || m_map[tileX-1][tileY]->blocksLOS()
										|| m_map[tileX][tileY-1]->blocksLOS() || m_map[tileX][tileY]->blocksLOS())
									isBlocked = true;
							}
						}
					} else {
						if(signY == -1)
							tileY--;
						if(m_map[tileX][tileY]->blocksLOS() || m_map[tileX-1][tileY]->blocksLOS())
						isBlocked = true;
					}
				}
			}
			if(dy != 0 && !isBlocked) {
				for(int y = 1; y < dy && !isBlocked; y++) {
					int x = y*dx/dy;
					int corner = y*dx%dy;
					int tileX = startX+x*signX, tileY = startY+y*signY;
					if(corner == 0) {
						if(tileX+1 >= m_cols || tileY+1 >= m_rows || tileX <= 0 || tileY <= 0
								|| (m_map[tileX][tileY]->blocksLOS() && m_map[tileX][tileY-1]->blocksLOS())
								|| (m_map[tileX][tileY]->blocksLOS() && m_map[tileX-1][tileY]->blocksLOS())
								|| (m_map[tileX-1][tileY-1]->blocksLOS() && m_map[tileX][tileY-1]->blocksLOS())
								|| (m_map[tileX-1][tileY-1]->blocksLOS() && m_map[tileX-1][tileY]->blocksLOS())) {
							isBlocked = true;
						} else {
							if(signX*signY == 1) {
								if(m_map[tileX][tileY]->blocksLOS() || m_map[tileX-1][tileY-1]->blocksLOS())
									isBlocked = true;
							} else if(signX*signY == -1) {
								if(m_map[tileX][tileY-1]->blocksLOS() || m_map[tileX-1][tileY]->blocksLOS())
									isBlocked = true;
							} else {
								if(m_map[tileX-1][tileY-1]->blocksLOS() || m_map[tileX-1][tileY]->blocksLOS()
										|| m_map[tileX][tileY-1]->blocksLOS() || m_map[tileX][tileY]->blocksLOS())
									isBlocked = true;
							}
						}
					} else {
						if(signX == -1)
							tileX--;
						if(m_map[tileX][tileY]->blocksLOS() || m_map[tileX][tileY-1]->blocksLOS())
							isBlocked = true;
					}
				}
			}
			if(!isBlocked)
				return true;
		}
	}
	return false;
}

void Level::calculateFOV(int charX, int charY, int viewRadius, std::set<Tile*>& visibleTiles) const {
	int i, j;
	int cornerSize = 2*viewRadius+2;
	bool** cornersVisible = new bool*[cornerSize];
	for(i = 0; i < cornerSize; i++) {
		cornersVisible[i] = new bool[cornerSize];
		for(j = 0; j < cornerSize; j++)
			cornersVisible[i][j] = false;
	}
	
	for(i = 0; i < cornerSize; i++) {
		for(j = 0; j < cornerSize; j++) {
			if((i == viewRadius && j == viewRadius) || (i == viewRadius+1 && j == viewRadius) || (i == viewRadius+1 && j == viewRadius+1) || (i == viewRadius && j == viewRadius+1)) {
				cornersVisible[i][j] = true;
				continue;
			}

			int endX = charX+i-viewRadius, endY = charY+j-viewRadius;
			int checkBoundsX = endX, checkBoundsY = endY;
			if(endX - charX > 0)
				checkBoundsX--;
			if(endY - charY > 0)
				checkBoundsY--;
			if(checkBoundsX >= 0 && checkBoundsX < m_cols && checkBoundsY >= 0 && checkBoundsY < m_rows) {
				bool visible = lineOfSight(charX, charY, endX, endY, viewRadius);
				cornersVisible[i][j] = visible;
			}
		}
	}

	for(i = 0; i < cornerSize; i++) {
		for(j = 0; j < cornerSize; j++) {
			if(cornersVisible[i][j]) {
				int cornerX = charX+i-viewRadius, cornerY = charY+j-viewRadius;
				if(cornerY-1 >= 0 && cornerX-1 >= 0)
					visibleTiles.insert(m_map[cornerX-1][cornerY-1]);
				if(cornerX < m_cols && cornerY-1 >= 0)
					visibleTiles.insert(m_map[cornerX][cornerY-1]);
				if(cornerX < m_cols && cornerY < m_rows)
					visibleTiles.insert(m_map[cornerX][cornerY]);
				if(cornerX-1 >= 0 && cornerY < m_rows)
					visibleTiles.insert(m_map[cornerX-1][cornerY]);
			}
		}
	}

	int singleCheck[][4] = {
		-1,-1, -2,-2,
		1,1,	2,2,
		-1,1,  -2,2,
		1,-1,	2,-2,
		-1,0,  -2,0,
		1,0,    2,0,
		0,-1,   0,-2,
		0,1,	0,2
	};
	for(i = 0; i < 8; i++) {
		if(charX+singleCheck[i][2] >= 0 && charX+singleCheck[i][2] < m_cols && charY+singleCheck[i][3] >= 0 && charY+singleCheck[i][3] < m_rows && m_map[charX+singleCheck[i][0]][charY+singleCheck[i][1]]->blocksLOS())
			visibleTiles.erase(m_map[charX+singleCheck[i][2]][charY+singleCheck[i][3]]);
	}

	int doubleCheck[][6] = {
		-1,-1, 0,-1, -1,-2,
		1,-1,  0,-1,  1,-2,
		-1,1,  0,1,  -1,2,
		1,1,   0,1,   1,2,
		-1,-1,-1,0,  -2,-1,
		-1,1, -1,0,  -2,1,
		1,-1,  1,0,   2,-1,
		1,1,   1,0,   2,1
	};

	for(i = 0; i < 8; i++) {
		if(charX+doubleCheck[i][4] >= 0 && charX+doubleCheck[i][4] < m_cols && charY+doubleCheck[i][5] >= 0 && charY+doubleCheck[i][5] < m_rows
				&& m_map[charX+doubleCheck[i][0]][charY+doubleCheck[i][1]]->blocksLOS()
				&& m_map[charX+doubleCheck[i][2]][charY+doubleCheck[i][3]]->blocksLOS()) {
			visibleTiles.erase(m_map[charX+doubleCheck[i][4]][charY+doubleCheck[i][5]]);
		}
	}

	for(i = 0; i < cornerSize; i++) {
		delete[] cornersVisible[i];
	}
	delete[] cornersVisible;
}
